import {IContext, ICreateNodeData, ICreateStageOptions, INodeData, ThemeName} from "./interfaces.ts"
import {NodeWrapper} from "./NodeWrapper.tsx"
import {createNodeImpl, eachNode, makeUUID, restoreHierarchy, saveHierarchy} from "./utils.ts"
import {CtrlLayer} from "./CtrlLayer.tsx"
import {CommandManager} from "./CommandManager.ts";

const DEFAULT_OPTIONS: Partial<ICreateStageOptions> = {
  theme: 'light',
  autoResize: true,
  editMode: false,
}

export class Stage {
  protected _ctx: IContext
  protected _options: ICreateStageOptions
  protected _host: HTMLElement
  protected _root: NodeWrapper
  protected _ctrlLayer: CtrlLayer | undefined
  protected _commands = new CommandManager()

  get root() {
    return this._root
  }

  get pureRoot() {
    return saveHierarchy(this._root)
  }

  get host() {
    return this._host
  }

  get options() {
    return this._options
  }

  get ctrl() {
    return this._ctrlLayer
  }

  get commands() {
    return this._commands
  }

  constructor(options: ICreateStageOptions) {
    this._options = Object.assign({}, DEFAULT_OPTIONS, options)

    const {
      el,
      theme,
      autoResize,
      editMode,
      nodes,
    } = this._options as Required<ICreateStageOptions>

    const host: HTMLElement | null = el ? typeof el === 'string' ? document.querySelector(el) : el : null

    if (!host) {
      throw new Error('need option[el]')
    }
    this._host = host

    host.classList.add('easy-design')

    this.changeTheme(theme, false)

    this._ctx = {
      options: this._options,
      theme,
      stage: this,
      editMode,
    } as IContext

    let root: NodeWrapper | undefined
    if (nodes) {
      root = restoreHierarchy(this, nodes)
      //console.log(root)
    }
    if (!nodes || !root) {
      root = new NodeWrapper()
    }
    this._root = root
    const rootEl = root.$el
    rootEl.classList.add('node-root')
    host.appendChild(root.$el)

    if (editMode) {
      const ctrlLayer = this._ctrlLayer = new CtrlLayer(this)
      host.appendChild(ctrlLayer.$el)

      this._commands.init(
        this,
        {
          actionCallback: ctrlLayer.$historyCallback,
          historyChangeCallback: ctrlLayer.$historyChangeCallback,
        }
      )
    }

    this.resize()
    if (autoResize) {
      window.addEventListener("resize", this.resize)
    }
  }

  changeTheme(theme: ThemeName, invoke = true) {
    this._host.setAttribute('data-theme', theme)

    if (invoke) {
      eachNode(this._root, (childNode) => {
        childNode.$invokeThemeChanged(theme)
      })
    }
  }

  resize = () => {
    if (this._options.editMode) {
      const {_host, _root, _options: {designBounds}} = this
      const {x, y, w, h} = designBounds ?? {}
      const rootEl = _root.$el
      const hostBounds = _host.getBoundingClientRect()
      rootEl.style.left = (x ?? 0) + 'px'
      rootEl.style.top = (y ?? 0) + 'px'
      rootEl.style.width = (w ?? hostBounds.width) + 'px'
      rootEl.style.height = (h ?? hostBounds.height) + 'px'
    }

    eachNode(this._root, (childNode) => {
      childNode.$invokeResize()
    })
  }

  $createNode(createNodeData: ICreateNodeData) {
    const {_ctx: ctx} = this
    const nodeData = createNodeData as INodeData
    if (!nodeData.id) {
      nodeData.id = makeUUID(this._root)
    }
    let nodeWrapper = new NodeWrapper()
    if (nodeData.nodeType === 'group') {
      nodeWrapper.$init(ctx, this, nodeData)
    } else {
      const nodeImpl = createNodeImpl(ctx, nodeData)
      if (nodeImpl) {
        nodeWrapper.$init(ctx, this, nodeData, nodeImpl)
      } else {
        console.warn(`create node[type: ${nodeData.nodeType}] failed`)
      }
    }
    return nodeWrapper
  }

  $addNode(nodeData: ICreateNodeData, parentNodeId?: number, insertIndex?: number) {
    let parentNode: NodeWrapper | undefined
    if (parentNodeId) {
      eachNode(this._root, childNode => {
        if (childNode.id === parentNodeId) {
          parentNode = childNode
          return true
        }
      })
    } else {
      parentNode = this._root
    }
    if (parentNode) {
      const node = parentNode.addChildWithData(nodeData, insertIndex)
      return node
    } else {
      throw new Error(`parent node [id: ${parentNodeId}] not found`)
    }
  }

  $removeNode(nodeId: number) {
    let nodeData: INodeData | undefined
    let parentNodeId: number | undefined
    let insertIndex: number | undefined
    eachNode(this._root, (childNode, index, parentNode) => {
      if (childNode.id === nodeId) {
        nodeData = childNode.nodeData
        parentNodeId = parentNode?.id
        insertIndex = index
        childNode.removeFromParent()
        return true
      }
    })
    return {
      nodeData,
      parentNodeId,
      insertIndex,
    }
  }

  $invokeAddNode(node: NodeWrapper) {
    const {onAddNode} = this._options
    onAddNode && onAddNode(node.nodeData)
  }

  $invokeRemoveNode(node: NodeWrapper) {
    const {onRemoveNode} = this._options
    onRemoveNode && onRemoveNode(node.nodeData)
    this._ctrlLayer?.selectNode(node, {
      multiMode: true,
      toggleMode: false,
      unselect: true,
    })
  }

  setHoverNode(id: number) {
    this._ctrlLayer?.setHoverNode(id)
  }

  destroy() {
    window.removeEventListener("resize", this.resize)
    eachNode(this._root, (childNode) => {
      childNode.removeFromParent()
    })

    this._host.appendChild(this._root.$el)
  }
}
